6.3 方法集
类型有一个与之相关的方法集(method set),这决定了它是否实现某个接口。
- 类型T方法集包含所有receiver T方法。
- 类型*\T方法集包含所有receiver T+*\T方法。
- 匿名嵌入S,T方法集包含所有receiver S方法。
- 匿名嵌入*\S,T方法集包含所有receiver S+*S方法。
- 匿名嵌入S或*\S,*\T方法集包含所有receiver S+*\S方法。
可利用反射(reflect)测试这些规则。
type S struct{}
type T struct{
S // 匿名嵌入字段
}
func(S)sVal() {}
func(*S)sPtr() {}
func(T)tVal() {}
func(*T)tPtr() {}
func methodSet(a interface{}) { // 显示方法集里所有方法名字
t:=reflect.TypeOf(a)
for i,n:=0,t.NumMethod();i<n;i++ {
m:=t.Method(i)
fmt.Println(m.Name,m.Type)
}
}
func main() {
var t T
methodSet(t) // 显示T方法集
println("----------")
methodSet(&t) // 显示 *T方法集
}
输出:
sVal func(main.T)
tVal func(main.T)
----------------------
sPtr func(*main.T)
sVal func(*main.T)
tPtr func(*main.T)
tVal func(*main.T)
输出结果符合预期,但我们也注意到某些方法的receiver类型发生了改变。真实情况是,这些都是由编译器按方法集所需自动生成的额外包装方法。
$nm test|grep"main\."
0000000000002040 t main.S.sVal
0000000000002050 t main.(*S).sPtr
00000000000023b0 t main.(*S).sVal
0000000000002570 t main.T.sVal
0000000000002060 t main.T.tVal
0000000000002490 t main.(*T).sPtr
0000000000002450 t main.(*T).sVal
0000000000002070 t main.(*T).tPtr
00000000000024d0 t main.(*T).tVal
$go tool objdump-s"main\."test|grep"TEXT.*autogenerated"
TEXT main.(*S).sVal(SB) <autogenerated>
TEXT main.T.sVal(SB) <autogenerated>
TEXT main.(*T).sVal(SB) <autogenerated>
TEXT main.(*T).sPtr(SB) <autogenerated>
TEXT main.(*T).tVal(SB) <autogenerated>
方法集仅影响接口实现和方法表达式转换,与通过实例或实例指针调用方法无关。实例并不使用方法集,而是直接调用(或通过隐式字段名)。
很显然,匿名字段就是为方法集准备的。否则,完全没必要为少写个字段名而大费周章。
面向对象的三大特征“封装”、“继承”和“多态”,Go仅实现了部分特征,它更倾向于“组合优于继承”这种思想。将模块分解成相互独立的更小单元,分别处理不同方面的需求,最后以匿名嵌入方式组合到一起,共同实现对外接口。而且其简短一致的调用方式,更是隐藏了内部实现细节。
组合没有父子依赖,不会破坏封装。且整体和局部松耦合,可任意增加来实现扩展。各单元持有单一职责,互无关联,实现和维护更加简单。
尽管接口也是多态的一种实现形式,但我认为应该和基于继承体系的多态分离开来。